package com.gusto.engine.semsim.measures.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.log4j.Logger;

import com.gusto.engine.beans.exceptions.PropertyAccessException;
import com.gusto.engine.beans.model.Res;
import com.gusto.engine.semsim.exceptions.SimilarityException;
import com.gusto.engine.semsim.measures.ResourceSimilarity;
import com.gusto.engine.semsim.measures.SetSimilarity;
import com.gusto.engine.semsim.measures.Similarity;
import com.gusto.engine.semsim.measures.ValueSimilarity;
import com.gusto.engine.semsim.measures.types.SemTypes;

/**
 * <p>The Ressemblance between two sets, is given by calculating the similarity
 * between all the items of the two sets, and then selecting the best ones.<br/>
 * The {@code elementSimilarity} property defines the measure used to 
 * calculate the similarity between 2 elements of the 2 sets.<br/>
 * Warning: This similarity calculates {@code n * m} similarities,
 * consider that in selecting the {@code elementSimilarity} measure.</p>
 * 
 * @author amokrane.belloui@gmail.com
 */
public class RessemblanceSetSimilarity implements SetSimilarity {
	
	private Logger log = Logger.getLogger(getClass());
	
	private List<? extends Object> stopwords = new ArrayList<Object>();
	public void setStopwords(List<? extends Object> stopwords) {
		this.stopwords = stopwords;
		log.debug(stopwords);
	}
	
	private SemTypes type;
	
	private Similarity elementSimilarity;
	public void setElementSimilarity(Similarity elementSimilarity) throws SimilarityException {
		this.elementSimilarity = elementSimilarity;
		if (type.equals(SemTypes.RESOURCE)) {
			if (! (elementSimilarity instanceof ResourceSimilarity)) {
				log.fatal("similarity measure " + elementSimilarity + " doesn't implement " + ResourceSimilarity.class);
				throw new SimilarityException("similarity measure " + elementSimilarity + " doesn't implement " + ResourceSimilarity.class);
			}
		}
		else if (type.equals(SemTypes.VALUE)) {
			if (! (elementSimilarity instanceof ValueSimilarity)) {
				log.fatal("similarity measure " + elementSimilarity + " doesn't implement " + ValueSimilarity.class);
				throw new SimilarityException("similarity measure " + elementSimilarity + " doesn't implement " + ValueSimilarity.class);
			}
		}
	}
	
	public String getId() {
		String id = "[" + getClass().getName() + "|" + type + "|" + 
			elementSimilarity.getId() + "]";
		return id;
	}
	
	public RessemblanceSetSimilarity(SemTypes type) {
		this.type = type;
	}
	
	public RessemblanceSetSimilarity(SemTypes type, Similarity elementSimilarity) throws SimilarityException {
		this.type = type;
		setElementSimilarity(elementSimilarity);
		
	}
	
	private class Sim implements Comparable<Sim> {
		public Integer id;
		public Double sim;
		public Object value1;
		public Object value2;
		public Sim(Integer id, Double sim, Object value1, Object value2) {
			super();
			this.id = id;
			this.sim = sim;
			this.value1 = value1;
			this.value2 = value2;
		}
		public int compareTo(Sim o) {
			if (this.sim < o.sim)      { return 1; }
			else if (this.sim > o.sim) { return -1;	}
			else {
				if (this.id > o.id)      { return 1; }
				else if (this.id < o.id) { return -1; }
				else                     { return 0; }
			}
		}
	}
	
	public Double getSimilarity(List<? extends Object> list1, List<? extends Object> list2) throws SimilarityException, PropertyAccessException {
		list1.removeAll(stopwords);
		list2.removeAll(stopwords);
		
		List<Sim> theSims = new ArrayList<Sim>();
		List<Object> theItems1 = new ArrayList<Object>(list1);
		List<Object> theItems2 = new ArrayList<Object>(list2);
		
		int k = 0;
		for (Object value1 : list1) {
			for (Object value2 : list2) {
				if (type.equals(SemTypes.RESOURCE)) {
					Res r1 = (Res)value1;
					Res r2 = (Res)value2;
					
					ResourceSimilarity resourceSimilarity = (ResourceSimilarity)elementSimilarity;
					Double sim = resourceSimilarity.getSimilarity(r1, r2);
					log.debug(r1 + " " + r2 + " " + sim);
					
					theSims.add(new Sim(k, sim, r1, r2));
					k++;
				}
				else if (type.equals(SemTypes.VALUE)) {
					//Literal l1 = (Literal)value1;
					//Literal l2 = (Literal)value2;
					
					ValueSimilarity valueSimilarity = (ValueSimilarity)elementSimilarity;
					Double sim = valueSimilarity.getSimilarity(value1, value2);
					log.debug(value1 + " " + value2 + " " + sim);
					
					theSims.add(new Sim(k, sim, value1, value2));
					k++;
				}
			}
		}
		// Processing
		// Normalized between [0, 1]
		Double totalSimilarity = 0D;
		
		Collections.sort(theSims);
		log.debug("Selecting best matchs ...");
		log.debug("Items in the list " + theItems1 + " " + theItems2);
		for (Sim s : theSims) {
			if (theItems1.contains(s.value1) && theItems2.contains(s.value2)) {
				log.debug("Selected Match " + s.id + ": " + s.sim + "; " + s.value1 + "-" + s.value2);
				totalSimilarity += s.sim;
				theItems1.remove(s.value1);
				theItems2.remove(s.value2);
				log.debug("Items in the list " + theItems1 + " " + theItems2);
			}
		}
		
		Double sim = new Double(totalSimilarity * 2.0) / new Double(list1.size() + list2.size());
		log.debug("Ressemblance, List1 " + list1);
		log.debug("Ressemblance, List2 " + list2);
		log.debug("Ressemblance, Sim " + sim);
		log.info("Set ressemblance " + sim + ", total:" + totalSimilarity);
		
		return sim;
	}
	
	public SemTypes getType() {
		return this.type;
	}
	
}
